package jp.co.nes.awssupport.business.logic;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;

import jp.co.nes.awssupport.access.dao.CompileProgressDao;
import jp.co.nes.awssupport.access.dao.CompileProgressHeaderDao;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.CompileProgressBean;
import jp.co.nes.awssupport.common.bean.CompileProgressHeaderBean;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;

/**
 * 集計進捗状況管理です。<br>
 * 集計進捗状況ヘッダ、集計進捗状況(連結アカウント単位の処理に関する集計進捗状況)を管理するクラスです。
 */
public class CompileProgressManage {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(CompileProgressManage.class);

    /** 連結アカウント単位の処理に関する完了状態 */
    public enum CompletionState {
        /** 集計処理トピック送信完了 */
        TOPIC_PUBLISH,

        /** 集計処理呼び出し完了 */
        PROCESS_START,

        /** スポット管理出力完了 */
        SPOT_MANAGE,

        /** 日時集計結果出力完了 */
        DAILY_COMPILED_BILLING,

        /** 融通リザーブド管理出力完了 */
        BLENDED_RESERVED_MANAGE,

        /** 月次集計結果出力完了 */
        MONTHLY_COMPILED_BILLING,

        /** キャンペーンプラン適用完了 */
        CAMPAIGN_PLAN,

        /** クレジット適用完了 */
        CREDIT_COUPON,

        /** 雑費適用完了 */
        MISCELLANEOUS_EXPENSES_PLAN,

        /** プレミアムサポート適用完了 */
        PREMIUM_SUPPORT,

        /** 税金プラン適用完了 */
        TAX_PLAN;

        /**
         * 完了日時を返却するGetterメソッド名を作成します。
         *
         * @return 完了日時を返却するGetterメソッド名
         */
        public String cerateCompletionDateGetterMethodName() {
            return create("get");
        }

        /**
         * 完了日時を設定するSetterメソッド名を作成します。
         *
         * @return 完了日時を設定するSetterメソッド名
         */
        public String createCompletionDateSetterMethodName() {
            return create("set");
        }

        /**
         * 完了日時を返却するGetterメソッド名 or 完了日時を設定するSetterメソッド名を作成します。
         *
         * @param prefix 作成するメソッド名の先頭に付加する文字列(「get」or「set」)
         * @return 完了日時を返却するGetterメソッド名 or 完了日時を設定するSetterメソッド名
         */
        private String create(String prefix) {
            if (!"get".equals(prefix) && "!set".equals(prefix)) {
                throw new CommonException("prefix is wrong(prefix=" + prefix + ")");
            }

            StringBuilder builder = new StringBuilder();
            builder.append(prefix);

            boolean isLowerCase = false;
            String name = name();
            for (int i = 0; i < name.length(); i++) {
                char ch = name.charAt(i);
                if (ch == '_') {
                    isLowerCase = false;
                } else {
                    if (isLowerCase) {
                        builder.append(Character.toLowerCase(ch));
                    } else {
                        builder.append(ch);
                        isLowerCase = true;
                    }
                }
            }
            builder.append("CompletionDate");
            return builder.toString();
        }
    };

    /** 集計進捗状況の各種件数をマップに設定する時に使用するキー名 */
    public enum CountKey {
        /** 集計進捗状況の全件数 */
        ALL_COUNT,

        /** 集計処理呼び出しが完了している件数 */
        COMPLETION_PROCESS_START_ALL_COUNT,

        /** 集計処理呼び出しが失敗している件数 */
        FAILURE_PROCESS_START_ALL_COUNT,

        /** 集計処理が完了している件数 */
        COMPLETION_PROCESS_ALL_COUNT,

        /** 集計処理が失敗している件数 */
        FAILURE_PROCESS_ALL_COUNT,

        /** 集計処理の状態が不明な件数 */
        UNKNOWN_ALL_COUNT
    }

    /** 処理対象年月 */
    private YearMonth targetYearMonth;

    /**
     * 処理対象年月を引数にもつコンストラクタです。
     *
     * @param targetYearMonth 処理対象年月
     */
    public CompileProgressManage(YearMonth targetYearMonth) {
        this.targetYearMonth = targetYearMonth;
    }

    /**
     * 集計進捗状況管理開始処理です。
     */
    public static void start() {
        logger.info("CompileProgressManage start");

        String categoryHeader = "CompileProgressHeader";
        String category = "CompileProgress";
        long rch = AppInfoUtil.getLongValAndMinChk(categoryHeader, "CreateTableReadCapacity", 5L, 0L);
        long wch = AppInfoUtil.getLongValAndMinChk(categoryHeader, "CreateTableWriteCapacity", 5L, 0L);
        long rc = AppInfoUtil.getLongValAndMinChk(category, "CreateTableReadCapacity", 10L, 0L);
        long wc = AppInfoUtil.getLongValAndMinChk(category, "CreateTableWriteCapacity", 10L, 0L);

        if (logger.isDebugEnabled()) {
            logger.debug(categoryHeader + ",CreateTableReadCapacity=" + rch);
            logger.debug(categoryHeader + ",CreateTableWriteCapacity=" + wch);
            logger.debug(category, ",CreateTableReadCapacity=" + rc);
            logger.debug(category + ",CreateTableWriteCapacity=" + wc);
        }

        // テーブル作成
        logger.info("dynamodb create table start");
        CompileProgressHeaderDao headerDao = new CompileProgressHeaderDao();
        CompileProgressDao dao = new CompileProgressDao();
        headerDao.createTable(rch, wch);
        dao.createTable(rc, wc);
        logger.info("dynamodb create table end");

        // データを削除する。
        // なお、本メソッドが使用されるタイミング(通常運用時)から削除するオブジェクトのリスト取得について、
        // ConsistentReadで呼び出す必要なしと判断する。
        logger.info("dynamodb getList start");
        List<CompileProgressHeaderBean> deleteHeaderList = headerDao.getList();
        if (logger.isDebugEnabled()) {
            logger.debug("deleteHeaderList size=" + deleteHeaderList.size());
        }
        List<CompileProgressBean> deleteList = dao.getList();
        if (logger.isDebugEnabled()) {
            logger.debug("deleteList size=" + deleteList.size());
        }
        logger.info("dynamodb getList end");

        logger.info("dynamodb delete start");
        headerDao.delete(deleteHeaderList);
        dao.delete(deleteList);
        logger.info("dynamodb delete end");

        logger.info("CompileProgressManage end");
    }

    /**
     * 集計進捗状況管理の初期データを作成します。
     *
     * @param targetYearMonth 処理対象年月
     * @param linkedAccountIdList 連結アカウントIDリスト
     */
    public static void createData(YearMonth targetYearMonth, List<String> linkedAccountIdList) {
        logger.info("createData start");
        CompileProgressHeaderDao headerDao = new CompileProgressHeaderDao();
        CompileProgressDao dao = new CompileProgressDao();

        // 保存するBeanの作成
        CompileProgressHeaderBean saveCphBean = new CompileProgressHeaderBean();
        saveCphBean.setTargetYearMonth(targetYearMonth);
        List<CompileProgressBean> saveList = new ArrayList<>();
        for (String linkedAccountId : linkedAccountIdList) {
            CompileProgressBean cpBean = new CompileProgressBean();
            cpBean.setTargetYearMonth(targetYearMonth);
            cpBean.setLinkedAccountId(linkedAccountId);
            saveList.add(cpBean);
        }

        // 保存する。
        logger.info("dynamodb save start");
        headerDao.save(saveCphBean);
        dao.save(saveList);
        logger.info("dynamodb save end");

        logger.info("createData end");
    }

    /**
     * 集計進捗状況の各種件数を返却します。
     *
     * @return 集計進捗状況の各種件数を設定したマップ
     */
    public static Map<CountKey, Integer> getAllCount() {
        // 確実に最新の情報を取得したいのでConsistentReadで呼び出す。
        CompileProgressDao dao = new CompileProgressDao();
        List<CompileProgressBean> cpList = dao.getListWithConsistentRead();

        Map<CountKey, Integer> resultMap = new HashMap<>();
        resultMap.put(CountKey.ALL_COUNT, cpList.size());
        resultMap.put(CountKey.COMPLETION_PROCESS_START_ALL_COUNT,
                cpList.stream()
                        .filter(cpBean -> cpBean.getProcessStartCompletionDate() != null)
                        .collect(Collectors.toList())
                        .size());
        resultMap.put(CountKey.FAILURE_PROCESS_START_ALL_COUNT,
                cpList.stream()
                        .filter(cpBean -> cpBean.getProcessStartFailureDate() != null)
                        .collect(Collectors.toList())
                        .size());
        resultMap.put(CountKey.COMPLETION_PROCESS_ALL_COUNT,
                cpList.stream()
                        .filter(cpBean -> cpBean.getTaxPlanCompletionDate() != null)
                        .collect(Collectors.toList())
                        .size());
        resultMap.put(CountKey.FAILURE_PROCESS_ALL_COUNT,
                cpList.stream()
                        .filter(cpBean -> cpBean.getProcessFailureDate() != null)
                        .collect(Collectors.toList())
                        .size());
        resultMap.put(CountKey.UNKNOWN_ALL_COUNT, getUnknownCount(cpList));
        return resultMap;
    }

    /**
     * 呼び出した処理の状態が不明な件数を返却します。
     *
     * @param cpList 集計進捗状況リスト
     * @return 呼び出した処理の状態が不明な件数
     */
    private static int getUnknownCount(List<CompileProgressBean> cpList) {
        String category = "CompileProgress";
        int maxStart = AppInfoUtil.getIntValAndMinChk(category, "MaxProcessStartCompletionPossibilityTime", 3600000, 0);
        int maxBatch = AppInfoUtil.getIntValAndMinChk(category, "MaxBatchCompletionPossibilityTime", 3600000, 0);
        int maxLambda = AppInfoUtil.getIntValAndMinChk(category, "MaxLambdaCompletionPossibilityTime", 1800000, 0);
        if (logger.isDebugEnabled()) {
            logger.debug(category + ",MaxProcessStartCompletionPossibilityTime=" + maxStart);
            logger.debug(category + ",MaxBatchCompletionPossibilityTime=" + maxBatch);
            logger.debug(category + ",MaxLambdaCompletionPossibilityTime=" + maxLambda);
        }

        // 処理が開始されていて、処理が正常、異常に関わらず終了していないリストを取得。
        List<CompileProgressBean> filterList = cpList.stream()
                .filter(cpBean -> cpBean.getProcessStartCompletionDate() != null)
                .filter(cpBean -> cpBean.getTaxPlanCompletionDate() == null)
                .filter(cpBean -> cpBean.getProcessFailureDate() == null)
                .collect(Collectors.toList());
        if (logger.isDebugEnabled()) {
            logger.debug("filterList.size()=" + filterList.size());
        }

        int unknownAllCount = 0;
        CompletionState[] completionStateArray = CompletionState.values();
        for (CompileProgressBean cpBean : filterList) {

            // 集計進捗状況に含まれる完了日時のうち、現時点で、一番最新のものを取得する。
            CompletionState curCompletionState = null;
            ZonedDateTime curValue = null;
            for (CompletionState completionState : completionStateArray) {
                if (completionState == CompletionState.TOPIC_PUBLISH || completionState == CompletionState.TAX_PLAN) {
                    // 集計処理トピック送信完了と税金適用の完了日時は判断に使用しないのでcontinueする。
                    continue;
                }

                try {
                    String methodName = completionState.cerateCompletionDateGetterMethodName();
                    Method method = CompileProgressBean.class.getMethod(methodName);
                    ZonedDateTime value = (ZonedDateTime) method.invoke(cpBean);
                    if (value != null) {
                        if (curValue == null) {
                            curValue = value;
                        } else {
                            if (curValue.isBefore(value)) {
                                curCompletionState = completionState;
                                curValue = value;
                            }
                        }
                    }
                } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                        | InvocationTargetException e) {
                    throw new CommonException(e);
                }
            }

            ZonedDateTime nowValue = DateUtil.getNowOfZonedDateTime();
            Date now = DateUtil.toDate(nowValue);
            Date cur = DateUtil.toDate(curValue);
            long time = now.getTime() - cur.getTime();

            if (logger.isDebugEnabled()) {
                logger.debug("CompletionState=" + curCompletionState + "now=" + now.getTime() + ",cur=" + cur.getTime()
                        + ",time=" + time);
            }

            int max = 0;
            if (curCompletionState == CompletionState.PROCESS_START) {
                max = maxStart;
            } else if (curCompletionState == CompletionState.DAILY_COMPILED_BILLING) {
                max = maxBatch;
            } else {
                max = maxLambda;
            }
            if (time > max) {
                // 集計進捗状況に含まれる完了日時のうち、現時点で、一番最新のものが、最大値より時間が経過している場合
                if (logger.isDebugEnabled()) {
                    logger.debug("curCompletionState=" + curCompletionState + "time=" + time + ",max=" + max);
                }
                unknownAllCount++;
            }
        }

        return unknownAllCount;
    }

    /**
     * 引数の連結アカウントに該当する内容を集計進捗状況から削除します。
     *
     * @param linkedAccountId 連結アカウントID
     */
    public void delete(String linkedAccountId) {
        String category = "CompileProgress";
        int sleepTime = AppInfoUtil.getIntValAndMinChk(category, "DeleteSleepTime", 1000, 0);
        int maxLoopCount = AppInfoUtil.getIntValAndMinChk(category, "DeleteMaxLoopCount", 10, 0);
        int loopCount = 0;
        boolean isLoop = true;
        while (isLoop) {
            // 本メソッドが使用されるタイミング(通常運用時)から削除するオブジェクトの取得について、
            // ConsistentReadで呼び出す必要なしと判断する。
            CompileProgressDao dao = new CompileProgressDao();
            CompileProgressBean cpBean = dao.get(linkedAccountId, targetYearMonth);
            if (cpBean == null) {
                throw new CommonException("CompileProgressBean is not exist");
            }
            try {
                dao.delete(cpBean);
                isLoop = false;
            } catch (ConditionalCheckFailedException e) {
                // バージョン番号に関するエラー、条件付き制約に関するエラーが発生した場合。
                // ただ、ここでは、条件付き制約は使用していないので、バージョン番号に関するエラー時に、
                // 再実行することを想定している。
                if (loopCount < maxLoopCount) {
                    sleep(sleepTime);
                    continue;
                } else {
                    // 削除失敗
                    throw new CommonException(e);
                }
            }
        }
    }

    /**
     * 請求CSVが確定しているか返却します。<br>
     * なお、複数の一括請求アカウントの処理を実施している場合、全ての一括請求アカウントの請求CSVが、
     * 確定している場合にtrueで返却します。
     *
     * @return true: 確定, false: 未確定
     */
    public boolean isConfirmedBillingCsv() {
        // 確実に最新の情報を取得したいのでConsistentReadで呼び出す。
        CompileProgressHeaderDao headerDao = new CompileProgressHeaderDao();
        CompileProgressHeaderBean cphBean = headerDao.getWithConsistentRead(targetYearMonth);
        if (cphBean == null) {
            throw new CommonException("CompileProgressHeaderBean is not exist");
        }
        return cphBean.isConfirmedBillingCsv();
    }

    /**
     * Sforceへの反映を実施してOKかどうかを返却します。
     *
     * @return true: OK, false OKでない。
     */
    public boolean isOkSforceUpdate() {
        List<String> sforceUpdateLinkedAccountIdList = getOkSforceUpdateLinkedAccountIdList();
        return !sforceUpdateLinkedAccountIdList.isEmpty();
    }

    /**
     * Sforceへの反映を実施する連結アカウントIDリストを返却します。
     *
     * @return Sforceへの反映を実施する連結アカウントIDリスト
     */
    public List<String> getOkSforceUpdateLinkedAccountIdList() {
        // 確実に最新の情報を取得したいのでConsistentReadで呼び出す。
        CompileProgressDao dao = new CompileProgressDao();
        List<CompileProgressBean> cpList = dao.getListWithConsistentRead(targetYearMonth);
        if (cpList.isEmpty()) {
            // 連結アカウントIDが0件の時は、Sforceへの反映を実施しない。
            logger.debug("cpList is empty(sforceUpdateLinkedAccountIdList.size()=0)");
            return new ArrayList<>();
        }

        int countYearMonth = cpList.size();
        List<CompileProgressBean> taxPlanCompletionList = cpList.stream()
                .filter(cpBean -> cpBean.getTaxPlanCompletionDate() != null)
                .collect(Collectors.toList());
        int completionProcessCount = taxPlanCompletionList.size();
        int failureProcessCount = cpList.stream()
                .filter(cpBean -> cpBean.getProcessFailureDate() != null)
                .collect(Collectors.toList())
                .size();
        int unknownCount = getUnknownCount(cpList);
        int countExec = completionProcessCount + failureProcessCount + unknownCount;

        if (logger.isDebugEnabled()) {
            StringBuilder builder = new StringBuilder();
            builder.append("countYearMonth=");
            builder.append(countYearMonth);
            builder.append(",completionProcessCount=");
            builder.append(completionProcessCount);
            builder.append(",failureProcessCount=");
            builder.append(failureProcessCount);
            builder.append(",unknownCount=");
            builder.append(unknownCount);
            logger.debug(builder.toString());
        }

        List<String> sforceUpdateLinkedAccountIdList = null;
        if (countYearMonth == countExec) {
            // 正常に税金プラン適用が完了しているものだけ、Sforceへの反映を実施する。
            // なお、税金プラン適用完了日時設定後の処理で、予期しない異常が発生し、
            // Step Functionsでエラーが発生していた場合、Step Functions自体はエラーになっているが、
            // Sforce反映自体は実施される。
            // ただ、このような状況は、ほとんど発生しないと考えられ、仮に反映されても、
            // データ自体は適切な内容になっていると考えられる(リカバリーする場合でも再実行すれば良いだけ)。
            // そのため、特に問題なしと考えている。
            sforceUpdateLinkedAccountIdList = taxPlanCompletionList.stream()
                    .map(cpBean -> cpBean.getLinkedAccountId())
                    .collect(Collectors.toList());
        } else if (countYearMonth > countExec) {
            // Sforceへの反映を実施しない。
            // この条件に当てはまるのは、以下の場合。
            // ・トピック送信を含めて、それ以前で失敗しているアカウントが存在する。
            // ・税金プラン適用完了日時、集計処理失敗日時、いずれにも値が入っていないアカウントが存在する。
            sforceUpdateLinkedAccountIdList = new ArrayList<>();
        } else {
            // countYearMonth < countExecの場合。
            // 通常発生しない。
            throw new CommonException(
                    "count wrong(countYearMonth=" + countYearMonth + ", countExec=" + countExec + ")");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("sforceUpdateLinkedAccountIdList.size()=" + sforceUpdateLinkedAccountIdList.size());
        }

        return sforceUpdateLinkedAccountIdList;
    }

    /**
     * 集計進捗状況を管理する連結アカウントIDのリストを返却します。
     *
     * @return 集計進捗状況を管理する連結アカウントIDリスト
     */
    public List<String> getLinkedAccountIdList() {
        logger.info("dynamodb getList start");
        // 本メソッドが使用されるタイミング(通常運用時)からConsistentReadでなくても問題ないと考えられるが、
        // 念のため、ConsistentReadで呼び出す。
        CompileProgressDao dao = new CompileProgressDao();
        List<CompileProgressBean> cpList = dao.getListWithConsistentRead(targetYearMonth);
        if (logger.isDebugEnabled()) {
            logger.debug("compileProgressList size=" + cpList.size());
        }
        logger.info("dynamodb getList end");
        return cpList.stream().map(cpBean -> cpBean.getLinkedAccountId()).collect(Collectors.toList());
    }

    /**
     * 指定された連結アカウントIDに関する集計進捗状況が、指定された完了状態になっているかどうかを返却します。
     *
     * @param linkedAccountId 連結アカウントID
     * @param completionState 連結アカウント単位の処理に関する完了状態
     * @return true: 指定された完了状態になっている、false: 指定された完了状態になっていない。
     */
    public boolean isCompletion(String linkedAccountId, CompletionState completionState) {
        // 確実に最新の情報を取得したいのでConsistentReadで呼び出す。
        CompileProgressDao dao = new CompileProgressDao();
        CompileProgressBean cpBean = dao.getWithConsistentRead(linkedAccountId, targetYearMonth);
        if (cpBean == null) {
            throw new CommonException("CompileProgressBean is not exist");
        }

        boolean isCompletion = false;
        String methodName = completionState.cerateCompletionDateGetterMethodName();
        ZonedDateTime value = getZonedDateTimeField(cpBean, methodName);
        if (value != null) {
            isCompletion = true;
        }
        return isCompletion;
    }

    /**
     * 請求CSVが確定していると集計進捗状況ヘッダに設定します。<br>
     * なお、複数の一括請求アカウントの処理を実施している場合、全ての一括請求アカウントの請求CSVが、
     * 確定している場合に本メソッドを呼び出します。
     */
    public void confirmedBillingCsv() {
        String category = "CompileProgressHeader";
        int sleepTime = AppInfoUtil.getIntValAndMinChk(category, "SaveSleepTime", 1000, 0);
        int maxLoopCount = AppInfoUtil.getIntValAndMinChk(category, "SaveMaxLoopCount", 10, 0);
        int loopCount = 0;
        boolean isLoop = true;
        while (isLoop) {
            // 本メソッドが使用されるタイミング(通常運用時)からConsistentReadでなくても問題ないと考えられるが、
            // 念のため、ConsistentReadで呼び出す。
            CompileProgressHeaderDao headerDao = new CompileProgressHeaderDao();
            CompileProgressHeaderBean cphBean = headerDao.getWithConsistentRead(targetYearMonth);
            if (cphBean == null) {
                throw new CommonException("CompileProgressHeaderBean is not exist");
            }
            cphBean.setConfirmedBillingCsv(true);

            try {
                headerDao.save(cphBean);
                isLoop = false;
            } catch (ConditionalCheckFailedException e) {
                // バージョン番号に関するエラー、条件付き制約に関するエラーが発生した場合。
                // ただ、ここでは、条件付き制約は使用していないので、バージョン番号に関するエラー時に、
                // 再実行することを想定している。
                if (loopCount < maxLoopCount) {
                    sleep(sleepTime);
                    continue;
                } else {
                    // 更新失敗
                    throw new CommonException(e);
                }
            }
        }
    }

    /**
     * 指定された連結アカウントID、完了状態を集計進捗状況に設定します。
     *
     * @param linkedAccountId 連結アカウントID
     * @param completionState 連結アカウント単位の処理に関する完了状態
     */
    public void completion(String linkedAccountId, CompletionState completionState) {
        String methodName = completionState.createCompletionDateSetterMethodName();
        saveZonedDateTimeField(linkedAccountId, methodName);
    }

    /**
     * 指定された連結アカウントIDの集計進捗状況を集計処理呼び出し失敗状態にします。
     *
     * @param linkedAccountId 連結アカウントID
     */
    public void failureProcessStart(String linkedAccountId) {
        saveZonedDateTimeField(linkedAccountId, "setProcessStartFailureDate");
    }

    /**
     * 指定された連結アカウントIDの集計進捗状況を集計処理失敗状態にします。
     *
     * @param linkedAccountId 連結アカウントID
     */
    public void failureProcess(String linkedAccountId) {
        saveZonedDateTimeField(linkedAccountId, "setProcessFailureDate");
    }

    /**
     * 集計進捗状況から指定された値を返却します。
     *
     * @param cpBean 集計進捗状況
     * @param methodName メソッド名
     * @return 取得した値
     */
    private ZonedDateTime getZonedDateTimeField(CompileProgressBean cpBean, String methodName) {
        try {
            Method method = CompileProgressBean.class.getMethod(methodName);
            return (ZonedDateTime) method.invoke(cpBean);
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new CommonException(e);
        }
    }

    /**
     * 集計進捗状況ヘッダに指定された値を設定し、保存します。<br>
     * 現状、どこからも使用されていないメソッドですが、将来、CompileProgressHeaderにフィールドが追加され、
     * 値を保存するような処理が発生したときのために、削除せず、残しています。
     *
     * @param methodName メソッド名
     */
    @SuppressWarnings("unused")
    private void saveZonedDateTimeField(String methodName) {
        String category = "CompileProgressHeader";
        int sleepTime = AppInfoUtil.getIntValAndMinChk(category, "SaveSleepTime", 1000, 0);
        int maxLoopCount = AppInfoUtil.getIntValAndMinChk(category, "SaveMaxLoopCount", 10, 0);
        int loopCount = 0;
        boolean isLoop = true;
        while (isLoop) {
            // 本メソッドが使用されるタイミング(通常運用時)からConsistentReadでなくても問題ないと考えられるが、
            // 念のため、ConsistentReadで呼び出す。
            CompileProgressHeaderDao headerDao = new CompileProgressHeaderDao();
            CompileProgressHeaderBean cphBean = headerDao.getWithConsistentRead(targetYearMonth);
            if (cphBean == null) {
                throw new CommonException("CompileProgressHeaderBean is not exist");
            }

            Class<CompileProgressHeaderBean> cls = CompileProgressHeaderBean.class;
            setZonedDateTimeField(cls, cphBean, methodName, DateUtil.getNowOfZonedDateTime());
            try {
                headerDao.save(cphBean);
                isLoop = false;
            } catch (ConditionalCheckFailedException e) {
                // バージョン番号に関するエラー、条件付き制約に関するエラーが発生した場合。
                // ただ、ここでは、条件付き制約は使用していないので、バージョン番号に関するエラー時に、
                // 再実行することを想定している。
                if (loopCount < maxLoopCount) {
                    sleep(sleepTime);
                    continue;
                } else {
                    // 更新失敗
                    throw new CommonException(e);
                }
            }
        }
    }

    /**
     * 集計進捗状況に指定された値を設定し、保存します。
     *
     * @param linkedAccountId 連結アカウントID
     * @param methodName メソッド名
     */
    private void saveZonedDateTimeField(String linkedAccountId, String methodName) {
        String category = "CompileProgress";
        int sleepTime = AppInfoUtil.getIntValAndMinChk(category, "SaveSleepTime", 1000, 0);
        int maxLoopCount = AppInfoUtil.getIntValAndMinChk(category, "SaveMaxLoopCount", 10, 0);
        int loopCount = 0;
        boolean isLoop = true;
        while (isLoop) {
            // 本メソッドが使用されるタイミング(通常運用時)からConsistentReadでなくても問題ないと考えられるが、
            // 念のため、ConsistentReadで呼び出す。
            CompileProgressDao dao = new CompileProgressDao();
            CompileProgressBean cpBean = dao.getWithConsistentRead(linkedAccountId, targetYearMonth);
            if (cpBean == null) {
                throw new CommonException("CompileProgressBean is not exist");
            }

            Class<CompileProgressBean> cls = CompileProgressBean.class;
            setZonedDateTimeField(cls, cpBean, methodName, DateUtil.getNowOfZonedDateTime());
            try {
                dao.save(cpBean);
                isLoop = false;
            } catch (ConditionalCheckFailedException e) {
                // バージョン番号に関するエラー、条件付き制約に関するエラーが発生した場合。
                // ただ、ここでは、条件付き制約は使用していないので、バージョン番号に関するエラー時に、
                // 再実行することを想定している。
                if (loopCount < maxLoopCount) {
                    sleep(sleepTime);
                    continue;
                } else {
                    // 更新失敗
                    throw new CommonException(e);
                }
            }
        }
    }

    /**
     * 指定されたBeanクラスのメソッドの値を設定します。
     *
     * @param cls 値を設定するBeanクラスのClassオブジェクト
     * @param bean 値を設定するBeanクラスのインスタンス
     * @param methodName 値を設定するメソッド
     * @param completionDate 設定する値
     */
    private <T> void setZonedDateTimeField(Class<T> cls, T bean, String methodName, ZonedDateTime completionDate) {
        try {
            Method method = cls.getMethod(methodName, ZonedDateTime.class);
            method.invoke(bean, completionDate);
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new CommonException(e);
        }
    }

    /**
     * スリープします。
     */
    private void sleep(int sleepTime) {
        try {
            if (sleepTime > 0) {
                Thread.sleep(sleepTime);
            }
        } catch (InterruptedException e) {
            // ログを出力して、処理を続行する。
            logger.warn(e.getMessage(), e);
        }
    }
}
